﻿using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Win32;
using System.Diagnostics;
using System.Net;

namespace Pst_Backup_Agent
{
    public class Utilities
    {
        private string _logSeverity = string.Empty;
        private int _logSeverityRank = 0;
        private bool _isLogActivated = false;
        private static string _settingsBaseRegistryKey = @"Software\PST Backup 2013\";

        internal string LogSeverity
        {
            get { return _logSeverity; }
            set
            {
                _logSeverity = value;
                LogSeverityRank = GetLogSeverityRankFromString(_logSeverity);
            }
        }

        private int LogSeverityRank { get { return _logSeverityRank; } set { _logSeverityRank = value; } }

        internal bool IsLogActivated { get { return _isLogActivated; } set { _isLogActivated = value; } }

        /// <summary>
        /// A structure describing a registry entry.
        /// </summary>
        internal struct RegistryEntry
        {
            private string _fullRegistryPath;
            private string _sourcePath;
            private string _destinationPath;
            private bool _toBackup;
            private DateTime _lastSuccessfulBackup;

            internal RegistryEntry(string sourcePath)
            {
                _fullRegistryPath = string.Empty;
                _sourcePath = sourcePath;
                _destinationPath = string.Empty;
                _toBackup = true;
                _lastSuccessfulBackup = new DateTime();
            }

            internal string FullRegistryPath { get { return _fullRegistryPath; } set { _fullRegistryPath = value; } }
            internal string SourcePath { get { return _sourcePath; } set { _sourcePath = value; } }
            internal string DestinationPath { get { return _destinationPath; } set { _destinationPath = value; } }
            internal bool ToBackup { get { return _toBackup; } set { _toBackup = value; } }
            internal DateTime LastSuccessfulBackup { get { return _lastSuccessfulBackup; } set { _lastSuccessfulBackup = value; } }

            public override string ToString() { return this.SourcePath; }
        }

        /// <summary>
        /// Log a message in the Windows Event Log.
        /// </summary>
        /// <param name="eventID">ID for this event.</param>
        /// <param name="message">Text to write in the event.</param>
        /// <param name="messageSeverity">Severity of the message : 0=Debug, 1=Information, 2=Warning, 3= Error</param>
        /// <param name="eventType">Type of the event. default value is "Information".</param>
        /// <param name="logName">Name of the log where to write the event. Default value is "Application".</param>
        /// <param name="sourceName">Name of the source of the event. Default value is "Pst Backup 2013".</param>
        internal void LogEvent(int eventID, string message, int messageSeverity, EventLogEntryType eventType = EventLogEntryType.Information, string logName = "Application", string sourceName = "Pst Backup 2013")
        {
            try
            {
                if (this.IsLogActivated && (messageSeverity >= LogSeverityRank))
                {
                    EventLog eventLog = new EventLog(logName, ".", sourceName);
                    eventLog.WriteEntry(message, eventType, eventID);
                }
            }
            catch (Exception) { }
        }
        
        /// <summary>
        /// Gets all PST files that have been previously recorded in the registry for being processed by PST Backup 2013.
        /// </summary>
        /// <returns>A Lsit of <RegistryEntry>.</returns>
        internal static List<RegistryEntry> GetPstRegistryEntries()
        {
            List<RegistryEntry> registryEntries = new List<RegistryEntry>();

            try
            {
                RegistryKey baseKey = Registry.CurrentUser.OpenSubKey(_settingsBaseRegistryKey + "PST Files");

                string[] subKeyNames = baseKey.GetSubKeyNames();

                foreach (string subKeyName in subKeyNames)
                {
                    try
                    {
                        RegistryKey subKey = baseKey.OpenSubKey(subKeyName);
                        registryEntries.Add(GetPstRegistryEntry(subKey));
                        subKey.Close();
                    }
                    catch (Exception) { }
                }
                baseKey.Close();
            }
            catch (Exception) { }

            return registryEntries;
        }

        /// <summary>
        /// Build a new RegistryEntry with values under the provided subKey.
        /// </summary>
        /// <param name="subKey">SubKey that contains registry values to build the new RegistryEntry.</param>
        /// <returns>Return a new RegistryEntry initialized with values contains under the subKey.</returns>
        private static RegistryEntry GetPstRegistryEntry(RegistryKey subKey)
        {
            RegistryEntry regEntry = new RegistryEntry("");
            try
            {
                regEntry.FullRegistryPath = subKey.Name;
                regEntry.SourcePath = subKey.GetValue("SourcePath").ToString();
                regEntry.DestinationPath = subKey.GetValue("DestinationPath").ToString();
                regEntry.ToBackup = subKey.GetValue("ToBackup").ToString().ToLower() == "true" ? true : false;
                regEntry.LastSuccessfulBackup = Convert.ToDateTime(subKey.GetValue("LastSuccessfulBackup").ToString());
            }
            catch (Exception) { }

            return regEntry;
        }

        /// <summary>
        /// Inspect the candidateValue, if this one is between minValue and maxValue, then return candidateValue else return defaultValue.
        /// </summary>
        /// <param name="candidateValue">The candidate value to inspect.</param>
        /// <param name="minValue">Minimum value.</param>
        /// <param name="maxValue">Maximum value.</param>
        /// <param name="defaultValue">Default value to return if candidateValue is NOT between minValue and maxValue.</param>
        /// <returns>A value that is between minValue and maxValue.</returns>
        internal static int GetCorrectIntValue(int candidateValue, int minValue, int maxValue, int defaultValue)
        {
            if (candidateValue < minValue || candidateValue > maxValue)
                return defaultValue;
            else
                return candidateValue;
        }

        /// <summary>
        /// Replaces all supported shortcuts by the full-lenght string. (e.g. : %windowslogin% is replaces by the username used to open the Windows session)
        /// </summary>
        /// <param name="pathToTranslate">String where to search for shortcuts.</param>
        /// <returns>String whitout shortcuts.</returns>
        internal static string TranslateDestinationFolder(string pathToTranslate)
        {
            pathToTranslate = pathToTranslate.ToLower();

            pathToTranslate = pathToTranslate.Replace("%windowslogin%", GetWindowsLogon());

            return pathToTranslate;
        }

        /// <summary>
        /// Allows to get only the filename from the fullpath of a file.
        /// </summary>
        /// <param name="fullpath">The fullpath to the file.</param>
        /// <returns>The name of the file with extension.</returns>
        internal static string GetFilenameFromFullPath(string fullpath)
        {
            string result = string.Empty;

            try
            {
                System.IO.FileInfo fileInfo = new System.IO.FileInfo(fullpath);
                if (fileInfo.Exists)
                    result = fileInfo.Name;
            }
            catch (Exception) { }

            return result;
        }

        /// <summary>
        /// Delete the file.
        /// </summary>
        /// <param name="fullPath">Full path to the file to delete.</param>
        internal static void DeleteFile(string fullPath)
        {
            try
            {
                System.IO.File.Delete(fullPath);
            }
            catch (Exception) { }
        }

        /// <summary>
        /// Delete the folder.
        /// </summary>
        /// <param name="fullPath">Full path to the folder to delete.</param>
        internal static void DeleteFolder(string fullPath)
        {
            try
            {
                System.IO.Directory.Delete(fullPath, true);
            }
            catch (Exception) { }
        }

        /// <summary>
        /// Allows to get the username used to open the Windows session.
        /// </summary>
        /// <returns>The user name used to open the Windows session.</returns>
        internal static string GetWindowsLogon()
        {
            return Environment.UserName;
        }

        /// <summary>
        /// Allows to get the name of this computer.
        /// </summary>
        /// <returns>The name of this computer.</returns>
        internal static string GetComputerName()
        {
            string computerName = string.Empty;

            try
            {
                computerName = Environment.MachineName;
            }
            catch (Exception) { }

            return computerName;
        }

        /// <summary>
        /// Determine whether or not, the destination is on a WAN link. Based on the local IPV4 address, the remote IPV4 address and the subnet mask.
        /// </summary>
        /// <param name="destination">Hostname or IpAddress to Ping.</param>
        /// <returns>Return false if the IPV4 address of the destination is in the same subnet of the local IPV4 address (based on the subnet mask), return true other wise. Return true if the destination doesn't reply to ping query.</returns>
        public static bool IsWanLink(string destination, string additionalSubnets)
        {
            try
            {
                System.Net.NetworkInformation.Ping pingRequest = new System.Net.NetworkInformation.Ping();

                string hostName = GetHostName(destination);
                System.Net.NetworkInformation.PingReply pingReply = pingRequest.Send(hostName, 3000);
                if (pingReply.Status == System.Net.NetworkInformation.IPStatus.Success)
                {
                    System.Net.IPAddress remoteAddress = pingReply.Address;
                    if (remoteAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
                        return true;
                    System.Net.IPAddress localAddress = GetLocalAddress(remoteAddress);
                    if (localAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
                        return true;
                    System.Net.IPAddress subnetMask = GetSubnetMask(localAddress);
                    if (subnetMask.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
                        return true;

                    if (IsInSameSubnet(localAddress, remoteAddress, subnetMask))
                        return false;

                    foreach (string additionalSubnet in additionalSubnets.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
                    {
                        if (IsInSameSubnet(localAddress, additionalSubnet))
                            return false;
                    }
                }
            }
            catch (Exception) { }

            return true;
        }

        /// <summary>
        /// Get the hostname of a remote host from the fullpath.
        /// </summary>
        /// <param name="destination">The fullpath to a directory on the remote host.</param>
        /// <returns>Return the hostname of the remote host or 'LocalHost' if the destination start by x:\</returns>
        public static string GetHostName(string destination)
        {
            string result = "LocalHost";

            try
            {
                System.Text.RegularExpressions.Regex regExpLocalHost = new System.Text.RegularExpressions.Regex(@"^[A-Za-z]{1}:\\", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
                if (!regExpLocalHost.IsMatch(destination))
                {
                    System.Text.RegularExpressions.Regex regExpIpAddress = new System.Text.RegularExpressions.Regex(@"^\\\\[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\\", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
                    if (regExpIpAddress.IsMatch(destination))
                    {
                        string remoteHost = regExpIpAddress.Match(destination).Captures[0].ToString();
                        return remoteHost.Substring(2, remoteHost.LastIndexOf('\\') - 2);
                    }
                    System.Text.RegularExpressions.Regex regExpRemoteHost = new System.Text.RegularExpressions.Regex(@"^\\\\[A-Za-z0-9\.]+\\", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
                    if (regExpRemoteHost.IsMatch(destination))
                    {
                        string remoteHost = regExpRemoteHost.Match(destination).Captures[0].ToString();
                        return remoteHost.Substring(2, remoteHost.LastIndexOf('\\') - 2);
                    }
                }
            }
            catch (Exception) { }

            return result;
        }

        /// <summary>
        /// Remove spaces at start and end of the string. Remove final '\' if any.
        /// </summary>
        /// <param name="folder">String to format.</param>
        /// <returns>Formatted string.</returns>
        public static string GetFormattedFolder(string folder)
        {
            folder = folder.Trim();
            if (folder.EndsWith("\\"))
                folder = folder.Substring(0, folder.Length - 1);

            return folder;
        }

        /// <summary>
        /// Get the local IPV4 address used to connect to the specified remote host.
        /// </summary>
        /// <param name="remoteAddress">IPV44 address of the remote host.</param>
        /// <returns>The IPV4 address used to connect to the specified remote host. Return null if no suitable local IPV4 address has been found.</returns>
        /// <exception cref="ArgumentException">Argument Exception if remoteAddress is not an IPV4 Address.</exception>
        public static IPAddress GetLocalAddress(IPAddress remoteAddress)
        {
            IPAddress localAddress = null;

            if (remoteAddress == null || remoteAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
                throw new ArgumentException("'remoteAddress' must be an IPV4 address.");

            try
            {
                System.Net.Sockets.UdpClient u = new System.Net.Sockets.UdpClient(remoteAddress.ToString(), 1);
                IPEndPoint ipEndpoint = (IPEndPoint)u.Client.LocalEndPoint;
                localAddress = ((IPEndPoint)u.Client.LocalEndPoint).Address;
                if (localAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
                    return null;
            }
            catch (Exception) { }

            return localAddress;
        }

        /// <summary>
        /// Get the subnet mask associated with an IPV4 address.
        /// </summary>
        /// <param name="localAddress">IPV4 address for which you will find the subnet mask.</param>
        /// <returns>The subnet mask associated with the IPV4 address.</returns>
        /// <exception cref="ArgumentException">Argument Exception if localAddress is not an IPV4 address.</exception>
        public static IPAddress GetSubnetMask(IPAddress localAddress)
        {
            if (localAddress == null || localAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
                throw new ArgumentException("'localAddress' must be an IPV4 address.");

            try
            {
                foreach (System.Net.NetworkInformation.NetworkInterface nic in System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces())
                {
                    System.Net.NetworkInformation.IPInterfaceProperties ipProps = nic.GetIPProperties();
                    foreach (System.Net.NetworkInformation.UnicastIPAddressInformation unicastAddress in ipProps.UnicastAddresses)
                    {
                        if (unicastAddress.Address.Equals(localAddress))
                            return unicastAddress.IPv4Mask;
                    }
                }
            }
            catch (Exception) { }
            return null;
        }

        /// <summary>
        /// Determine whether or not localAddress is in the same subnet than remoteAddress, based on the subnet mask.
        /// </summary>
        /// <param name="localAddress">The local IPV4 address.</param>
        /// <param name="remoteAddress">The remote IPV4 address.</param>
        /// <param name="subnetMask">The IPV4 subnet mask.</param>
        /// <returns>True if both localAddress and remoteAddress are in the same subnet based on the subnet mask, false other wise.</returns>
        /// <exception cref="ArgumentException">All IPAddress must be IPV4 addresses.</exception>
        /// <exception cref="NullException">When failed to get the subnet address for one of the IPV4 address.</exception>
        public static bool IsInSameSubnet(IPAddress localAddress, IPAddress remoteAddress, IPAddress subnetMask)
        {
            IPAddress localSubnet = GetSubnetAddress(localAddress, subnetMask);
            IPAddress remoteSubnet = GetSubnetAddress(remoteAddress, subnetMask);

            return localSubnet.Equals(remoteSubnet);
        }

        /// <summary>
        /// Determine Wheter or not the localAddress is in the same subnet.
        /// </summary>
        /// <param name="localAddress">IPAddress to look for.</param>
        /// <param name="subnet">Subnet into which we want to look for. Must be CIDR formatted</param>
        /// <returns>true if the localAddress is in tha sae subnet, false otherwise.</returns>
        public static bool IsInSameSubnet(IPAddress localAddress, string CIDRsubnet)
        {
            IPAddress subnet = GetIPAddressFromCIDRSubnet(CIDRsubnet);
            IPAddress mask = GetSubnetMaskFromCIDRSubnet(CIDRsubnet);

            return IsInSameSubnet(localAddress, subnet, mask);
        }

        public static IPAddress GetIPAddressFromCIDRSubnet(string CIDRSubnet)
        {
            if (CIDRSubnet.Contains("/"))
            {
                string ipAdd = CIDRSubnet.Substring(0, CIDRSubnet.IndexOf('/'));
                string[] strBytes = ipAdd.Split(new char[] { '.' });
                if (strBytes.Length == 4)
                {
                    byte[] bytes = new byte[strBytes.Length];

                    for (int i = 0; i < strBytes.Length; i++)
                    {
                        if (!byte.TryParse(strBytes[i], out bytes[i]))
                            return null;
                    }
                    return new IPAddress(bytes);
                }
            }

            return null;
        }

        public static IPAddress GetSubnetMaskFromCIDRSubnet(string CIDRSubnet)
        {
            if (CIDRSubnet.Contains("/"))
            {
                int CIDRMask = 0;
                if (!int.TryParse(CIDRSubnet.Substring(CIDRSubnet.IndexOf('/') + 1), out CIDRMask))
                    return null;
                byte[] bytes = new byte[4];
                int index = 0;

                while (CIDRMask >= 8)
                {
                    bytes[index] = 255;
                    CIDRMask -= 8;
                    index++;
                }

                if (CIDRMask > 0)
                {
                    switch (CIDRMask)
                    {
                        case 1:
                            bytes[index] = 128;
                            break;
                        case 2:
                            bytes[index] = 192;
                            break;
                        case 3:
                            bytes[index] = 224;
                            break;
                        case 4:
                            bytes[index] = 240;
                            break;
                        case 5:
                            bytes[index] = 248;
                            break;
                        case 6:
                            bytes[index] = 252;
                            break;
                        case 7:
                            bytes[index] = 254;
                            break;
                        default:
                            break;
                    }
                }

                return new IPAddress(bytes);
            }

            return null;
        }

        /// <summary>
        /// Get the subnet address from an IPV4 address and is subnet mask.
        /// </summary>
        /// <param name="ipv4Address">IPV4 address from which you want to get the subnet address.</param>
        /// <param name="subnetMask">the subnet mask associated with the IPV4 address.</param>
        /// <returns>The subnet address or null if failed.</returns>
        /// <exception cref="ArgumentExecption">All argument must IPV4 addresses.</exception>
        public static IPAddress GetSubnetAddress(IPAddress ipv4Address, IPAddress subnetMask)
        {
            if (ipv4Address == null || subnetMask == null || ipv4Address.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork || subnetMask.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
                throw new ArgumentException("'ipv4Address' and 'subnetMask' must be both IPV4 address");

            try
            {
                byte[] address = ipv4Address.GetAddressBytes();
                byte[] mask = subnetMask.GetAddressBytes();

                if (address.Length == mask.Length)
                {
                    byte[] subnetAddress = new byte[mask.Length];

                    for (int i = 0; i < mask.Length; i++)
                    {
                        subnetAddress[i] = (byte)(address[i] & mask[i]);
                    }
                    return new IPAddress(subnetAddress);
                }
            }
            catch (Exception) { }

            return null;
        }

        private int GetLogSeverityRankFromString(string logSeverity)
        {
            switch (logSeverity.ToLower())
            {
                case "debug":
                    return 0;
                case "information":
                    return 1;
                case "warning":
                    return 2;
                case "error":
                    return 3;
                default:
                    return 0;
            }
        }

        internal static void CreateFolderWithNTFSPermissions(string folderFullPath, bool setExclusiveNTFSPermissions, string additionalFullControl, string additionalReadWrite, Utilities logger)
        {
            System.IO.DirectoryInfo folderToCreate = new System.IO.DirectoryInfo(folderFullPath);

            if (!folderToCreate.Parent.Exists)
            {
                folderToCreate.Parent.Create();
            }
            folderToCreate.Create();

            if (setExclusiveNTFSPermissions)
            {
                System.Security.AccessControl.DirectorySecurity folderACL = folderToCreate.GetAccessControl();
                folderACL.SetAccessRuleProtection(true, false);

                SIDHelper.AddReadWriteAccess(folderACL, Environment.UserName);

                System.Security.Principal.SecurityIdentifier systemSID = new System.Security.Principal.SecurityIdentifier(System.Security.Principal.WellKnownSidType.LocalSystemSid, null);
                SIDHelper.AddAccessRule(folderACL, SIDHelper.GetFullControlAccessRule(systemSID));

                if (!String.IsNullOrEmpty(additionalReadWrite))
                {
                    try
                    {
                        foreach (string trustee in additionalReadWrite.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
                        {
                            try
                            {
                                SIDHelper.AddReadWriteAccess(folderACL, trustee);
                            }
                            catch (Exception ex) { logger.LogEvent(20015, "Unable to set Read/Write permissions to destination folder. " + ex.Message, 3, EventLogEntryType.Error); }
                        }
                    }
                    catch (Exception ex) { logger.LogEvent(20015, "Unable to set Read/Write permissions to destination folder. " + ex.Message, 3, EventLogEntryType.Error); }
                }

                if (!String.IsNullOrEmpty(additionalFullControl))
                {
                    try
                    {
                        foreach (string trustee in additionalFullControl.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
                        {
                            try
                            {
                                SIDHelper.AddFullControlAccess(folderACL, trustee);
                            }
                            catch (Exception ex) { logger.LogEvent(20016, "Unable to set Full Control permissions to destination folder. " + ex.Message, 3, EventLogEntryType.Error); }
                        }
                    }
                    catch (Exception ex) { logger.LogEvent(20016, "Unable to set Full Control permissions to destination folder. " + ex.Message, 3, EventLogEntryType.Error); }
                }

                folderToCreate.SetAccessControl(folderACL);
            }
        }

        /// <summary>
        /// Expand the %temp% Environnement Variable and add "\PstBackup\" + a random folder name (8 first character of a Guid) at the end.
        /// If %temp% can't be resolved, then 'LocalAppData' will be used instead.
        /// Create the directory if it doesn't exists.
        /// </summary>
        /// <returns>A path to a random folder with ending '\'.</returns>
        internal static string GetTempFolder()
        {
            string result = String.Empty;

            try
            {
                result = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);                
            }
            catch (Exception)
            {
                result = Environment.GetEnvironmentVariable("Temp");
            }
            result += "\\PstBackup\\" + Guid.NewGuid().ToString().Substring(0, 8) + "\\";

            if (!System.IO.Directory.Exists(result))
                System.IO.Directory.CreateDirectory(result);
            return result;
        }

    }
}
